# vPresent is an immersive presentation creation and display application.
# Copyright (C) 2007-2009 by Priority 5 Holdings, Inc.
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

import logging
import math

import gmtl
from PyQt4 import QtCore
import osg2 as osg


class Pipe(QtCore.QObject):
   def __init__(self, name, parent = None):
      QtCore.QObject.__init__(self, parent)

      self.name      = name
      self.id        = ''
      self.window    = None
      self.projector = None

   def getName(self):
      return self.name

   def setName(self, name):
      if self.name != name:
         self.name = name
         self.emit(QtCore.SIGNAL("nameChanged(QString*)"),
                   QtCore.QString(name))

   def getScreenID(self):
      return self.id

   def setScreenID(self, id):
      if self.id != id:
         self.id = id
         self.emit(QtCore.SIGNAL("screenIDChanged(QString*)"),
                   QtCore.QString(id))

   def hasWindow(self):
      return self.window is not None

   def getWindow(self):
      return self.window

   def setWindow(self, window):
      if self.window is not Window:
         old_window       = self.window
         self.window      = window
         self.window.pipe = self
         self.emit(QtCore.SIGNAL("windowChanged(QObject*,QObject*)"),
                   old_window, window)

   def hasProjector(self):
      return self.projector is not None

   def getProjector(self):
      return self.projector

   def setProjector(self, projector):
      if self.projector is not projector:
         old_projector  = self.projector
         self.projector = projector
         self.emit(QtCore.SIGNAL("projectorChanged(QObject*,QObject*)"),
                   old_projector, projector)

class Window(QtCore.QObject):
   def __init__(self, pipe, parent = None):
      QtCore.QObject.__init__(self, parent)

      self.pipe        = pipe
      self.width       = 400
      self.height      = 400
      self.x           = 200
      self.y           = 200
      self.fullScreen  = False
      self.alwaysOnTop = False
      self.stereo      = False
      self.border      = True

   def setScreenID(self, screenID):
      self.pipe.setScreenID(screenID)

   def getScreenID(self):
      return self.pipe.getScreenID()

   def setSize(self, width, height):
      self.setWidth(width)
      self.setHeight(height)

   def getSize(self):
      return (self.width, self.height)

   def setWidth(self, width):
      if self.width != width:
         self.width = width
         self.emit(QtCore.SIGNAL("widthChanged(int)"), width)

   def getWidth(self):
      return self.width

   def setHeight(self, height):
      if self.height != height:
         self.height = height
         self.emit(QtCore.SIGNAL("heightChanged(int)"), height)

   def getHeight(self):
      return self.height

   def setPosition(self, x, y):
      self.setX(x)
      self.setY(y)

   def getPosition(self):
      return (self.x, self.y)

   def setX(self, x):
      if self.x != x:
         self.x = x
         self.emit(QtCore.SIGNAL("positionXChanged(int)"), x)

   def getX(self):
      return self.x

   def setY(self, y):
      if self.y != y:
         self.y = y
         self.emit(QtCore.SIGNAL("positionYChanged(int)"), y)

   def getY(self):
      return self.y

   def setFullScreen(self, fullScreen):
      if self.fullScreen != fullScreen:
         self.fullScreen = fullScreen
         self.emit(QtCore.SIGNAL("fullScreenChanged(bool)"), fullScreen)

   def isFullScreen(self):
      return self.fullScreen

   def setAlwaysOnTop(self, alwaysOnTop):
      if self.alwaysOnTop != alwaysOnTop:
         self.alwaysOnTop = alwaysOnTop
         self.emit(QtCore.SIGNAL("alwaysOnTopChanged(bool)"), alwaysOnTop)

   def isAlwaysOnTop(self):
      return self.alwaysOnTop

   def setStereo(self, inStereo):
      if self.stereo != inStereo:
         self.stereo = inStereo
         self.emit(QtCore.SIGNAL("stereoChanged(bool)"), inStereo)

   def isStereo(self):
      return self.stereo

   def setHasBorder(self, hasBorder):
      if self.border != hasBorder:
         self.border = hasBorder
         self.emit(QtCore.SIGNAL("borderChanged(bool)"), hasBorder)

   def hasBorder(self):
      return self.border

   def __str__(self):
      return "Window on %(screenID)s:\n\tDimensions: %(width)dx%(height)d\n\tLocation: %(x)d, %(y)d\n\tFull screen: %(fullScreen)s\n\tAlways on top: %(alwaysOnTop)s\n\tIn stereo: %(stereo)s\n\tHas border: %(border)s" % self.__dict__

class Frustum(QtCore.QObject):
   '''
   This class represents the frustum used by a projector.
   '''
   def __init__(self, parent = None):
      QtCore.QObject.__init__(self, parent)

      self.left   = 0
      self.right  = 0
      self.bottom = 0
      self.top    = 0
      self.near   = 0
      self.far    = 0

   def get(self):
      '''
      get() -> (left, right, bottom, top, near, far)

      Returns this frustum as a tuple ordered as OpenSG's MatrixFrustum()
      function expects.
      '''
      return (self.left, self.right, self.bottom, self.top, self.near, self.far)

   def getLeft(self):
      return self.left

   def setLeft(self, left):
      if self.left != left:
         self.left = left
         self.emit(QtCore.SIGNAL("leftChanged(float)"), self.left)

   def getRight(self):
      return self.right

   def setRight(self, right):
      if self.right != right:
         self.right = right
         self.emit(QtCore.SIGNAL("rightChanged(float)"), self.right)

   def getBottom(self):
      return self.bottom

   def setBottom(self, bottom):
      if self.bottom != bottom:
         self.bottom = bottom
         self.emit(QtCore.SIGNAL("bottomChanged(float)"), self.bottom)

   def getTop(self):
      return self.top

   def setTop(self, top):
      if self.top != top:
         self.top = top
         self.emit(QtCore.SIGNAL("topChanged(float)"), self.top)

   def getNear(self):
      return self.near

   def setNear(self, near):
      if self.near != near:
         self.near = near
         self.emit(QtCore.SIGNAL("nearChanged(float)"), self.near)

   def getFar(self):
      return self.far

   def setFar(self, far):
      if self.far != far:
         self.far = far
         self.emit(QtCore.SIGNAL("farChanged(float)"), self.far)

   def __str__(self):
      return "  Left: %(left)s\n Right: %(right)s\nBottom: %(bottom)s\n   Top: %(top)s\n  Near: %(near)s\n   Far: %(far)s" % self.__dict__

class ProjectorParameter(QtCore.QObject):
   def __init__(self, initValue = QtCore.QVariant(), minimum = None,
                maximum = None, parent = None):
      QtCore.QObject.__init__(self, parent)
      #self.mLogger = logging.getLogger('display.ProjectorParameter')

      # Ensure that we duplicate the given object so that we own the object
      # referenced by self.value. This also ensures that we wrap whatever the
      # caller gave us in a QVariant.
      self.value = QtCore.QVariant(initValue)

      self.minimum = minimum
      self.maximum = maximum

   def hasMinimum(self):
      return self.minimum is not None

   def getMinimum(self):
      return self.minimum

   def hasMaximum(self):
      return self.maximum is not None

   def getMaximum(self):
      return self.maximum

   def set(self, value):
      # It is important that we copy value here so that we can retain an
      # object that we own. This also ensures that we wrap whatever the caller
      # gave us in a QVariant.
      new_value = QtCore.QVariant(value)

      if self.value != new_value:
         self.value = new_value
         self.emit(QtCore.SIGNAL("valueChanged(QVariant)"), self.value)

   def get(self):
      return self.value

class Projector(QtCore.QObject):
   '''
   Base projector type. Every Projector instance has at least a viewing
   frustum and a rotation (specified as an instance of osg.Quaternion)..
   '''

   def __init__(self, parent = None):
      QtCore.QObject.__init__(self, parent)
      self.frustum  = Frustum(self)
      self.rotation = gmtl.Matrix44f()

   def getFrustum(self):
      return self.frustum

   def getRotation(self):
      return self.rotation

class FovProjector(Projector):
   '''
   A projector type that has its view frustum configured using horizontal
   and vertical field of view settings. The horizontal field of view is
   split into left and right components. Similarly, the vertical field of view
   is split into bottom and top components.

   NOTE: The implementation of this class is very awkward. It is written the
   way that it is so that it can be plugged into a Qt table model easily for
   editing.
   '''

   HFOV_LEFT   = 0
   HFOV_RIGHT  = 1
   VFOV_BOTTOM = 2
   VFOV_TOP    = 3
   HEADING     = 4
   PITCH       = 5
   ROLL        = 6

   sDataMap = { HFOV_LEFT   : 'Horiz. FOV Left',
                HFOV_RIGHT  : 'Horiz. FOV Right',
                VFOV_BOTTOM : 'Vert. FOV Bottom',
                VFOV_TOP    : 'Vert. FOV Top',
                HEADING     : 'Heading',
                PITCH       : 'Pitch',
                ROLL        : 'Roll'
              }

   def __init__(self, near, far, thetaLeft, thetaRight, thetaBottom, thetaTop,
                heading, pitch, roll, parent = None):
      Projector.__init__(self, parent)

      self.connect(self.frustum, QtCore.SIGNAL("nearChanged(float)"),
                   self._updateFrustum)

      # Initialize basic data. The settings based on the given parameters will
      # happen below.
      self.mData = []
      for v in self.sDataMap.values():
         if v.find('FOV') != -1:
            self.mData.append(ProjectorParameter(0.0, -90.0, 90.0))
         else:
            self.mData.append(ProjectorParameter(0.0, -180.0, 180.0))

      # Block signal emission during construction.
      self.frustum.blockSignals(True)

      # These two need to be set manually because they are used in computing
      # other data (well, self.near is anyway).
      self.frustum.setNear(near)
      self.frustum.setFar(far)

      self.hFovLeft.set(thetaLeft)
      self.hFovRight.set(thetaRight)
      self.vFovBottom.set(thetaBottom)
      self.vFovTop.set(thetaTop)

      # Set the rotation information manually. This is done in this manner
      # so that _updateQuat() does not get called repeatedly.
      self.heading    = heading
      self.headingRad = math.radians(heading)
      self.pitch      = pitch
      self.pitchRad   = math.radians(pitch)
      self.roll       = roll
      self.rollRad    = math.radians(roll)

      # Now that we have everything in place, we call _updateFrustum() and
      # _updateQuat().
      self._updateFrustum(near)
      self._updateQuat()

      self.connect(self.hFovLeft, QtCore.SIGNAL("valueChanged(QVariant)"),
                   self._onHFovLeftChanged)
      self.connect(self.hFovRight, QtCore.SIGNAL("valueChanged(QVariant)"),
                   self._onHFovRightChanged)
      self.connect(self.vFovBottom, QtCore.SIGNAL("valueChanged(QVariant)"),
                   self._onVFovBottomChanged)
      self.connect(self.vFovTop, QtCore.SIGNAL("valueChanged(QVariant)"),
                   self._onVFovTopChanged)
      self.connect(self.heading, QtCore.SIGNAL("valueChanged(QVariant)"),
                   self._onHeadingChanged)
      self.connect(self.pitch, QtCore.SIGNAL("valueChanged(QVariant)"),
                   self._onPitchChanged)
      self.connect(self.roll, QtCore.SIGNAL("valueChanged(QVariant)"),
                   self._onRollChanged)

      # We are ready to go now.
      self.frustum.blockSignals(False)

   def makeDefault():
      return FovProjector(1.0, 10000.0, -30.0, 30.0, -30.0, 30.0, 0.0, 0.0,
                          0.0)
   makeDefault = staticmethod(makeDefault)

   def getType(self):
      return 'fov_projector'

   def getAttributes(self):
      return { 'near'        : self.frustum.getNear(),
               'far'         : self.frustum.getFar(),
               'hfov_left'   : self.hFovLeft.get().toDouble()[0],
               'hfov_right'  : self.hFovRight.get().toDouble()[0],
               'vfov_bottom' : self.vFovBottom.get().toDouble()[0],
               'vfov_top'    : self.vFovTop.get().toDouble()[0],
               'heading'     : self.heading.get().toDouble()[0],
               'pitch'       : self.pitch.get().toDouble()[0],
               'roll'        : self.roll.get().toDouble()[0]
             }

   def makeSettingsChangeHandler(self, settings):
      return FovProjectorSettingsChangeHandler(self, settings)

   def dataCount(self):
      return len(self.mData)

   def data(self, index, role = QtCore.Qt.DisplayRole):
      if index.isValid() and index.row() < len(self.mData):
         if role == QtCore.Qt.EditRole or QtCore.Qt.DisplayRole == role:
            if 0 == index.column():
               return QtCore.QVariant(self.sDataMap[index.row()])
            elif 1 == index.column():
               return self.mData[index.row()].get()

      return QtCore.QVariant()

   def setData(self, index, value, role = QtCore.Qt.DisplayRole):
      result = False
      if index.isValid() and index.row() < len(self.mData):
         if role == QtCore.Qt.EditRole:
            self.mData[index.row()].set(value)
            result = True

      return result

   def getParam(self, index):
      if index.isValid() and index.row() < len(self.mData):
         return self.mData[index.row()]

   def getDesc(self, index):
      if index.isValid() and index.row() < len(self.mData):
         return self.sDataMap[index.row()]

   def _onHFovLeftChanged(self, value):
      result = value.toDouble()
      if result[1]:
         theta = result[0]
         self.emit(QtCore.SIGNAL("horizontalFovLeftChanged(float)"), theta)
         self.frustum.setLeft(self._getFrustumValue(theta))
      else:
         raise Exception("Failed to convert variant %s to float" % \
                            str(value.toString()))

   def _onHFovRightChanged(self, value):
      result = value.toDouble()
      if result[1]:
         theta = result[0]
         self.emit(QtCore.SIGNAL("horizontalFovRightChanged(float)"), theta)
         self.frustum.setRight(self._getFrustumValue(theta))
      else:
         raise Exception("Failed to convert variant %s to float" % \
                            str(value.toString()))

   def _onVFovBottomChanged(self, value):
      result = value.toDouble()
      if result[1]:
         theta = result[0]
         self.emit(QtCore.SIGNAL("verticalFovBottomChanged(float)"), theta)
         self.frustum.setBottom(self._getFrustumValue(theta))
      else:
         raise Exception("Failed to convert variant %s to float" % \
                            str(value.toString()))

   def _onVFovTopChanged(self, value):
      result = value.toDouble()
      if result[1]:
         theta = result[0]
         self.emit(QtCore.SIGNAL("verticalFovTopChanged(float)"), theta)
         self.frustum.setTop(self._getFrustumValue(theta))
      else:
         raise Exception("Failed to convert variant %s to float" % \
                            str(value.toString()))

   def _onHeadingChanged(self, value):
      result = value.toDouble()
      if result[1]:
         heading         = result[0]
         self.headingRad = math.radians(heading)

         self.emit(QtCore.SIGNAL("headingChanged(float,float)"), heading,
                   self.headingRad)
         self._updateQuat()
      else:
         raise Exception("Failed to convert variant %s to float" % \
                            str(value.toString()))

   def _onPitchChanged(self, value):
      result = value.toDouble()
      if result[1]:
         pitch         = result[0]
         self.pitchRad = math.radians(pitch)

         self.emit(QtCore.SIGNAL("pitchChanged(float,float)"), pitch,
                   self.pitchRad)
         self._updateQuat()
      else:
         raise Exception("Failed to convert variant %s to float" % \
                            str(value.toString()))

   def _onRollChanged(self, value):
      result = value.toDouble()
      if result[1]:
         roll         = result[0]
         self.rollRad = math.radians(roll)

         self.emit(QtCore.SIGNAL("rollChanged(float,float)"), roll,
                   self.rollRad)
         self._updateQuat()
      else:
         raise Exception("Failed to convert variant %s to float" % \
                            str(value.toString()))

   def _getFrustumValue(self, angle):
      return self.frustum.getNear() * math.tan(math.radians(angle))

   def _updateFrustum(self, newNear):
      self.frustum.setLeft(
         self._getFrustumValue(self.mData[self.HFOV_LEFT].get().toDouble()[0])
      )
      self.frustum.setRight(self._getFrustumValue(
         self.mData[self.HFOV_RIGHT].get().toDouble()[0])
      )
      self.frustum.setBottom(
         self._getFrustumValue(self.mData[self.VFOV_BOTTOM].get().toDouble()[0])
      )
      self.frustum.setTop(
         self._getFrustumValue(self.mData[self.VFOV_TOP].get().toDouble()[0])
      )

   def _updateQuat(self):
      # This is just a precaution.
      if self.rotation is None:
         self.rotation = gmtl.Matrix44f()

      quat_x = gmtl.makeRotQuat(gmtl.AxisAnglef(self.pitchRad,
                                                gmtl.Vec3f(1, 0, 0)))
      quat_y = gmtl.makeRotQuat(gmtl.AxisAnglef(self.headingRad,
                                                gmtl.Vec3f(0, 1, 0)))
      quat_z = gmtl.makeRotQuat(gmtl.AxisAnglef(self.rollRad,
                                                gmtl.Vec3f(0, 0, 1)))

      rot_mat_x = gmtl.makeRotMatrix44(quat_x)
      rot_mat_y = gmtl.makeRotMatrix44(quat_y)
      rot_mat_z = gmtl.makeRotMatrix44(quat_z)

      mult_list = [rot_mat_y, rot_mat_x, rot_mat_z]

      rot_mat = gmtl.Matrix44f()
      for m in mult_list:
         rot_mat *= m

      dome_quat = gmtl.makeRotQuat(gmtl.AxisAnglef(math.radians(-25),
                                                   gmtl.Vec3f(1, 0, 0)))
      dome_mat  = gmtl.makeRotMatrix44(dome_quat)
      rot_mat   = dome_mat * rot_mat

      # Camera stuff is tricky, and this makes things work.
      gmtl.invert(rot_mat, rot_mat)

      self.rotation = rot_mat
      # TODO: Only emit this signal when there is an actual change.
      self.emit(QtCore.SIGNAL("rotationChanged()"))

   def __getattr__(self, name):
      if self.__dict__.has_key(name):
         return self.__dict__[name]
      elif name == 'hFovLeft':
         return self.mData[self.HFOV_LEFT]
      elif name == 'hFovRight':
         return self.mData[self.HFOV_RIGHT]
      elif name == 'vFovBottom':
         return self.mData[self.VFOV_BOTTOM]
      elif name == 'vFovTop':
         return self.mData[self.VFOV_TOP]
      elif name == 'heading':
         return self.mData[self.HEADING]
      elif name == 'pitch':
         return self.mData[self.PITCH]
      elif name == 'roll':
         return self.mData[self.ROLL]
      else:
         raise AttributeError("'FovProjector' object has no attribute '%s'" % \
                                 name)

   def __setattr__(self, name, value):
      if name == 'hFovLeft':
         self.mData[self.HFOV_LEFT].set(value)
      elif name == 'hFovRight':
         self.mData[self.HFOV_RIGHT].set(value)
      elif name == 'vFovBottom':
         self.mData[self.VFOV_BOTTOM].set(value)
      elif name == 'vFovTop':
         self.mData[self.VFOV_TOP].set(value)
      elif name == 'heading':
         self.mData[self.HEADING].set(value)
      elif name == 'pitch':
         self.mData[self.PITCH].set(value)
      elif name == 'roll':
         self.mData[self.ROLL].set(value)
      else:
         # NOTE: It is critical that we do things this way. Because
         # QtCore.QObject is a base class of this type, simplying doing
         # 'self.__dict__[name] = value' will have no effect.
         Projector.__setattr__(self, name, value)

class FovProjectorSettingsChangeHandler(QtCore.QObject):
   def __init__(self, projector, settings, parent = None):
      QtCore.QObject.__init__(self, parent)

      self.mProjector = projector
      self.mSettings  = settings

      self.mFovSlots = [(QtCore.SIGNAL("horizontalFovLeftChanged(float)"),
                         lambda fov: self.onFovChanged('hfov_left', fov)),
                        (QtCore.SIGNAL("horizontalFovRightChanged(float)"),
                         lambda fov: self.onFovChanged('hfov_right', fov)),
                        (QtCore.SIGNAL("verticalFovBottomChanged(float)"),
                         lambda fov: self.onFovChanged('vfov_bottom', fov)),
                        (QtCore.SIGNAL("verticalFovTopChanged(float)"),
                         lambda fov: self.onFovChanged('vfov_top', fov))
                       ]
      self.mRotateSlots = \
         [(QtCore.SIGNAL("headingChanged(float,float)"),
           lambda rotDeg, rotRad: self.onRotChanged('heading', rotDeg)),
          (QtCore.SIGNAL("pitchChanged(float,float)"),
           lambda rotDeg, rotRad: self.onRotChanged('pitch', rotDeg)),
          (QtCore.SIGNAL("rollChanged(float,float)"),
           lambda rotDeg, rotRad: self.onRotChanged('roll', rotDeg))
         ]

      for s in self.mFovSlots + self.mRotateSlots:
         self.connect(self.mProjector, s[0], s[1])

   def disconnectSlots(self):
      for s in self.mFovSlots + self.mRotateSlots:
         self.disconnect(self.mProjector, s[0], s[1])

   def onFovChanged(self, key, fov):
      self.mSettings[key] = fov

   def onRotChanged(self, key, rotDeg):
      self.mSettings[key] = rotDeg

DefaultProjector = FovProjector
